package com.flex.util {
   import flash.events.Event;
   import flash.events.IOErrorEvent;
   import flash.net.URLLoader;
   import flash.net.URLRequest;
   import flash.utils.Proxy;
   import flash.utils.flash_proxy;

   import mx.core.Application;
   import mx.core.FlexGlobals;
   import mx.logging.ILogger;
   import mx.messaging.ChannelSet;
   import mx.messaging.channels.AMFChannel;
   import mx.messaging.channels.SecureAMFChannel;
   import mx.rpc.AbstractOperation;
   import mx.rpc.AsyncToken;
   import mx.rpc.IResponder;
   import mx.rpc.remoting.RemoteObject;
   import mx.utils.URLUtil;

   use namespace flash_proxy;

   dynamic public class RemoteService extends Proxy {

      /** @private Default channel for all services. */
      private static var _defaultChannelUri:String = null;

      /* The default value of the flag indicating whether
      to use a secured channel for service calls. */
      private static var _defaultSecure:Boolean =
            URLUtil.isHttpsURL(FlexGlobals.topLevelApplication.url);

      /** @private RemoteService instances. */
      private static var _services:Object = {};

      /** @private Service destination to channel map. */
      private static var _registry:Object = {};

      /** @private Indicates that the service registry Url is still loading. */
      private static var _registryLoadInProgress:Boolean = false;

      /** @private Service calls that are waiting to be processed. */
      private var _pendingOperations:Array = null;

      /** @private Object representing the actual remote service. */
      private var _service:RemoteObject = null;

      /** @private Determines if secured (i.e. SSL) channel needs to be used. */
      private var _secure:Boolean;

      /**
       * @param secure Determines if secured (i.e. SSL) channel needs to be used.
       * By default set to null, in which case application url is used to determine
       * what channel to use. i.e. If application url contains https,
       * uses secured amf channel else uses the default amf channel.
       *
       * @return The one and only instance of ExampleSingleton.
       */
      public static function getInstance(destination:String, secure:Object=null):RemoteService {
         var service:RemoteService = _services[destination] as RemoteService;
         if (service == null) {
            service = new RemoteService(PrivateConstructorToken, destination, secure);
            _services[destination] = service;
         }

         return service;
      }

      public function RemoteService(token:Class, destination:String, secure:Object=null) {
         super();
         if (token != PrivateConstructorToken) {
            throw new Error("RemoteService cannot be instantiated directly.");
         }

         _service = new RemoteObject(destination);
         _secure = (secure != null) ? Boolean(secure) : _defaultSecure;

         if (_defaultChannelUri != null) {
            this.channel = _defaultChannelUri;
            return;
         }

         if (_registryLoadInProgress) {
            _pendingOperations = new Array();
            return;
         }

         this.channel = _registry[_service.destination];
      }

      public function set serviceChannelUri(uri:String):void {
         this.channel = uri;
      }

      public static function set serviceRegistryUrl(url:String):void {
         _registryLoadInProgress = true;

         var loader:URLLoader = new URLLoader();
         loader.addEventListener(Event.COMPLETE, onUrlLoadSuccess);
         loader.addEventListener(IOErrorEvent.IO_ERROR, onUrlLoadFailure);
         loader.load(new URLRequest(url));
      }

      public static function set defaultChannelUri(uri:String):void {
         _defaultChannelUri = uri;
      }

      public static function set defaultSecure(secure:Boolean):void {
         _defaultSecure = secure;
      }

      public static function get defaultSecure():Boolean {
         return _defaultSecure;
      }

      private static function onUrlLoadSuccess(event:Event):void {
         var loader:URLLoader = URLLoader(event.target);
         var services:XML = new XML(loader.data);
         for each(var serviceXml:XML in services.service) {
            _registry[serviceXml.@destination] = serviceXml.@channel;

            var service:RemoteService =
               _services[serviceXml.@destination];
            if (service != null) {
               service.channel = serviceXml.@channel;
               service.executePendingOperations();
            }
         }

         _registryLoadInProgress = false;
      }

      private static function onUrlLoadFailure(event:IOErrorEvent):void {
         for each (var service:RemoteService in _services) {
            service.executePendingOperations();
         }

         _registryLoadInProgress = false;
      }

      override flash_proxy function callProperty(name:*, ...rest):* {
         try {
            if (_registryLoadInProgress) {
               var asyncToken:AsyncToken = new AsyncToken(null);
               _pendingOperations.push(
                  new PendingOperation(name, rest, asyncToken));
               return asyncToken;
            }

            return _service.getOperation(name).send.apply(this, rest);
         }
         catch (e:Error) {
         }
      }

      override flash_proxy function getProperty(name:*):* {
         return _service.getOperation(name);
      }

      private function set channel(uri:String):void {
         if (uri == null || uri.length <= 0) {
            return;
         }

         var channelSet:ChannelSet = new ChannelSet();
         var amfChannel:AMFChannel;
         if (_secure) {
            amfChannel = new SecureAMFChannel("anyName", uri);
         } else {
            amfChannel = new AMFChannel("anyName", uri);
         }
         channelSet.addChannel(amfChannel);

         _service.channelSet = channelSet;
      }

      private function executePendingOperations():void {
         var length:int = _pendingOperations.length;
         for (var i:int = 0; i < length ; i++) {
            var operation:PendingOperation = PendingOperation(_pendingOperations[i]);
            var asyncToken:AsyncToken =
               _service.getOperation(operation.name).send.apply(this, operation.args);

            for each(var responder:IResponder in operation.asyncToken.responders) {
               asyncToken.addResponder(responder);
            }

            for (var property:String in operation.asyncToken) {
               asyncToken[property] = operation.asyncToken[property];
            }
         }

         _pendingOperations = [];
      }
   }
}

import mx.rpc.AbstractOperation;
import mx.rpc.AsyncToken;

class PendingOperation {
   /** A reference to the method to be called. */
   public var name:String;

   /** The arguments to be passed to the method. */
   public var args:Array /* of Object */;

   public var asyncToken:AsyncToken;

   public function PendingOperation(name:String, args:Array = null,
      asyncToken:AsyncToken = null) {
      this.name = name;
      this.args = args;
      this.asyncToken = asyncToken;
   }
}
class PrivateConstructorToken {}
